跳到主要内容

ES6 类和模块化

ES6 Class 类

在ES6中,class (类)作为对象的模板被引入,可以通过 class 关键字定义类。因为 class 的本质是 function 所以可以将其看作一个语法糖,让对象原型的写法更加清晰、更像面向对象编程的语法。

//定义一个类
class User{
name = ''

constructor(name) {
this.name = name;
}

hello(){
console.log("hello " + this.name + " and study JavaScript oop!");
}
}

// 继承
class Student extends User{
grade = 0;

constructor(name,grade) {
super(name);
this.grade = grade;
}

study(){
console.log(this.name + " Say: I am grade "+ this.grade + " and I like Learning~");
}
}

//使用这个类
let alsritter = new Student('alsritter',2);
alsritter.hello();
alsritter.study();

传统定义类的三种方法

参考资料 Javascript定义类(class)的三种方法(好老啊)

1、构造函数法:它用构造函数模拟"类",在其内部用 this 关键字指代实例对象

function Cat() {
  this.name = "大毛";
}

生成实例的时候,使用 new 关键字。

var cat1 = new Cat();
alert(cat1.name); // 大毛

类的属性和方法,还可以定义在构造函数的 prototype 对象之上。

Cat.prototype.makeSound = function(){
  alert("喵喵喵");
}

2、Object.create() :用这个方法,"类"就是一个对象,不是函数。(ES5 的语法)

var Cat = {
  name: "大毛",
  makeSound: function(){
alert("喵喵喵");
}
};

然后,直接用 Object.create() 生成实例,不需要用到 new

var cat1 = Object.create(Cat);
alert(cat1.name); // 大毛
cat1.makeSound(); // 喵喵喵

3、使用闭包的方式:这种方法不使用 thisprototype,代码部署起来非常简单,它也是用一个对象模拟"类"。在这个类里面,定义一个构造函数 createNew(),用来生成实例。

var Cat = {
  createNew: function(){
    var cat = {};
    cat.name = "大毛";
    cat.makeSound = function(){ alert("喵喵喵"); };
    return cat;
  }
};

// 使用的时候,调用 createNew() 方法,就可以得到实例对象。
var cat1 = Cat.createNew();
cat1.makeSound(); // 喵喵喵

基础用法

类表达式可以为匿名或命名。

// 匿名类
let Example = class {
constructor(a) {
this.a = a;
}
}

// 命名类
let Example = class Example {
constructor(a) {
this.a = a;
}
}

// 类声明
class Example {
constructor(a) {
this.a = a;
}
}

注意要点 类定义不会被提升,这意味着,必须在访问前对类进行定义,否则就会报错。

类中方法不需要 function 关键字。

类的属性

ES6 中,prototype 仍旧存在,虽然可以直接自类中定义方法,但是其实方法还是定义在 prototype 上的。 覆盖方法 / 初始化时添加方法

Example.prototype={
//methods
}

// 添加方法
Object.assign(Example.prototype,{
//methods
})

静态属性:class 本身的属性,即直接定义在类内部的属性( Class.propname ),不需要实例化。 ES6 中规定,Class 内部只有静态方法,没有静态属性。

class Example {
// 新提案
static a = 2;
}
// 目前可行写法
Example.b = 2;

公共属性

class Example{}
Example.prototype.a = 2;

实例属性

class Example {
a = 2;
constructor () {
console.log(this.a);
}
}

name 属性,返回跟在 class 后的类名(存在时)。

let Example = class Exam {
constructor(a) {
this.a = a;
}
}
console.log(Example.name); // Exam

let Example=class {
constructor(a) {
this.a = a;
}
}
console.log(Example.name); // Example

类的方法

constructor 方法是类的默认方法,创建类的实例化对象时被调用。

class Example{
constructor(){
console.log('我是 constructor');
}
}
new Example(); // 我是 constructor

在 ES2015/ES6 中引入了 class 关键字,只是语法糖,JavaScript 仍然是基于原型的,所以实际上这里定义的一个 class 也只是一个 “函数” 而已,例子如下

class Test {
constructor(){
// 默认返回实例对象 this
}
}
console.log(new Test() instanceof Test); // true

class Example {
constructor(){
// 指定返回对象
return new Test();
}
}
console.log(new Example() instanceof Example); // false

静态方法

class Example{
static sum(a, b) {
console.log(a+b);
}
}
Example.sum(1, 2); // 3

原型方法

class Example {
sum(a, b) {
console.log(a + b);
}
}
let exam = new Example();
exam.sum(1, 2); // 3

实例方法

class Example {
constructor() {
this.sum = (a, b) => {
console.log(a + b);
}
}
}

类的实例化

class 的实例化必须通过 new 关键字。(否则会报错)

class Example {
constructor(age) {
this.age = age;
}
}

let exam1 = new Example(18);

console.log(exam1); // Example { age: 18 }

这里的 new 关键字的原理

let exam1 = new Example(18);

JS 引擎在执行这句代码时,等价于如下的操作

function Example02(age) {
this.age = age;
}

var exam2 = {};
exam2.__proto__ = Example02.prototype;
// call 方法看上面
var result = Example02.call(exam2, 18);

console.log(exam2);

所以 exam2 的原型链是:exam2 -> Example02.prototype -> Object.prototype -> null

实例化对象

注意:它们是共享原型对象的,所以当原型改变了,这里被实例化的对象也改变了

class Example {
constructor(a, b) {
this.a = a;
this.b = b;
console.log('Example');
}
sum() {
return this.a + this.b;
}
}
let exam1 = new Example(2, 1);
let exam2 = new Example(3, 1);

console.log(exam1._proto_ == exam2._proto_); // true

exam1._proto_.sub = function() {
return this.a - this.b;
}

console.log(exam1.sub()); // 1
console.log(exam2.sub()); // 2

封装类属性

getter / setter(getter 与 setter 必须同级出现,不能单独出现)

class Example{
constructor(a, b) {
this.a = a; // 实例化时调用 set 方法
this.b = b;
}
get a(){
console.log('getter');
return this.a;
}
set a(a){
console.log('setter');
this.a = a; // 自身递归调用
}
}
let exam = new Example(1,2); // 不断输出 setter ,最终导致 RangeError

class Example1{
constructor(a, b) {
this.a = a;
this.b = b;
}
get a(){
console.log('getter');
return this._a;
}
set a(a){
console.log('setter');
this._a = a;
}
}
let exam1 = new Example1(1,2); // 只输出 setter , 不会调用 getter 方法
console.log(exam._a); // 1, 可以直接访问

extends 继承

使用 extends 关键字可以继承父类(其实还是原型链)

class Child extends Father { ... }

子类 constructor 方法中必须有 super,且必须出现在 this 之前。

class Father {
constructor() {
console.log('hello this your father');
}
}

class Child extends Father {
constructor(a) {
super();
this.a = a;
console.log(`hello this ${this.a}`);
}
}

let test = new Child(13);
// hello this your father
// hello this 13

同 Java 一样,super 就是指向其父类的指针

class Father {
test() {
return 'I am your father';
}
static test1() {
return 1;
}
}

class Child extends Father {
constructor() {
super();
}

test2() {
console.log(`this my father's return: ${super.test()}`);
}
}

let obj = new Child();
obj.test2(); // this my father's return: I am your father

也可以在静态方法中使用 super,指向其父类的静态方法

class Father {
static test1() {
return 1;
}
}

class Child2 extends Father {
constructor(){
super();
}
static test3(){
// 调用父类静态方法
return super.test1() + 2;
}
}

console.log(Child2.test3()); // 3

ES6 模块化

注意:这里是单独使用 babel 来进行打包,所以不依赖于 webpack

单独安装 babel

参考资料 babel 中文文档 参考资料 你真的会用 Babel 吗?

因为 NodeJS 无法直接使用 ES6 的模块化语法,所以需要使用 babel 来转成 CommonJS 规范

使用 npm 安装

# babel 不能直接使用 npm install babel 安装,因为 babel6 之后拆分成了几个核心包
npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/node
# 虽然这种装在测试环境的工具一般可以全局安装,但是为了不隐含依赖于正在使用的全局环境,使项目更具可移植性,一般还是本地安装。

# 再额外安装一个 polyfill
# (这个是为了解决一些不转码的全局 api的问题,具体参考 https://juejin.im/post/6844904063402770439)
npm install --save @babel/polyfill

因为 babel6 之后拆分成了几个核心包,所以下面进行分别介绍

@babel/core babel 的核心 api 都在这里面 @babel/cli 自带了一个内置的 CLI 命令行工具,可通过命令行编译文件(核心文件) @babel/preset-env 可以根据配置的目标浏览器或者运行环境来自动将ES2015+的代码转换为es5

创建配置文件

项目根目录创建文件 babel.config.js

const presets = [
[
"@babel/env",
{
targets: {
edge: "17",
firefox: "60",
chrome: "67",
safari: "11.1",
},
},
],
];

module.exports = { presets };

通过 npx 就可以直接在 NodeJS 里运行了

npx babel-node index.js

导入导出模块

// 导入模块不需要加 .js

import http from 'http'
import {default as http} from 'http'
import * as http from 'http'
import {get} from 'http'
// as 关键字和 sql 的那个 as 是一样的,就是别名
import {getList as get} from 'http'
import http, {getList} from 'fs'

export default http
export const http
export function getList
export {getList, get}
export * from 'http'

默认导出

参考资料 MDN--export 参考资料 开车去环游世界--ES6:export default 和 export 区别

export default 和 export 的区别

exportexport default 均可用于导出常量、函数、文件、模块等

可以在其它文件或模块中通过 import+(常量 | 函数 | 文件 | 模块) 名的方式,将其导入,以便能够对其进行使用

在一个文件或模块中,exportimport 可以有多个,export default 仅有一个

通过 export 方式导出,在导入时要加 { }export default 则不需要

// 1.export
//a.js
export const str = "blablabla~";
export function log(sth) {
return sth;
}
// 对应的导入方式:

//b.js
import { str, log } from 'a'; //也可以分开写两次,导入的时候带花括号




// 2.export default
//a.js
const str = "blablabla~";
export default str;
// 对应的导入方式:

//b.js
import str from 'a'; //导入的时候没有花括号




// 如果默认导出和按需导出同时存在
//a.js
export default {a, b, c}
export let s1 = 'aaa'
export let s2 = 'bbb'
export function say(){
console.log('hello ES6')
}
// 对应的导入方式:

//b.js 如果按需导入想自定义名字则使用 as 关键字
import a, {s1, s2 as ss2, say} from 'a'

直接导入并执行模板代码

如果想要单纯的执行某个模块中的代码,并不需要得到模块中向外暴露的成员,此时可以直接导入并执行模块代码

// 文件模块 m2.js
// 在当前模块中执行一个 for 循环
for(let i = 0; i < 3; i++){
console.log(i);
}


// 直接导入并执行代码
import 'm2'